/*
 RNAvigator is a tool for visualizing secondary structure of RNA
 Copyright 2012 Elias La Cour & Peter Bertelsen

 This file is part of RNAvigator.

 RNAvigator is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 RNAvigator is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with RNAvigator.  If not, see <http://www.gnu.org/licenses/>.
 */
package rnavigator.model;

import java.awt.geom.Arc2D;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.logging.Level;
import java.util.logging.Logger;
import rnavigator.controller.Controller;
import rnavigator.view.GUI;

/**
 * @author Elias la Cour & Peter Bertelsen
 */
public class Model {

    private boolean debug = false;
    public Structure structure = null;
    private ArrayList<Base> bases = null;
    private GUI gui;
    private Controller controller;

    public void createAndSetStructure(LoadFile loadFile) {

        structure = loadFile.getStructure();
        createStructure(structure);
        modelCircularFeynmanUnitCircle(structure);
    }

    /**
     * returns a structure containing a list of bases, list of knots, list of pseudo-knots
     *
     * @param structure takes a Structure as parameter
     */
    public Structure createStructure(Structure structure) {
        String[] dotSequence = structure.getDotSequence();
        String[] baseSequence = structure.getBaseSequence();
        bases = new ArrayList<Base>();
        LinkedList<Base> tmpBases = new LinkedList<Base>();
        LinkedList<Base> tmpPseudoBases = new LinkedList<Base>();
        //knots are sorted in no particular order
        LinkedList<Knot> knots = new LinkedList<Knot>();
        int nBases = baseSequence.length;
        int matePos;
        Base b, b2;

        //plots all info into bases array
        for (int i = 0, pos = 1; i < nBases; i++, pos++) {
            b = new Base(baseSequence[i], pos);
            b.setnBases(nBases);
            if (checkToDrawPosition(b)) {
                b.setDrawPos(true);
            }

            if (dotSequence[i].equals("(")) {
                b.setKnot(true);
                tmpBases.push(b);
                bases.add(b);
            }
            if (dotSequence[i].equals("[")) {
                b.setPseudoKnot(true);
                tmpPseudoBases.push(b);
                bases.add(b);
            }
            if (dotSequence[i].equals(")")) {
                b.setKnot(true);
                b2 = tmpBases.pop();
                matePos = b2.getPosition();
                b.setMate(matePos);
                bases.add(b);
                //sets the other end of the knot
                bases.get(matePos - 1).setMate(pos);
                knots.push(new Knot(matePos, pos));
                //System.out.println("Knot found between: " + b.getPosition() + " " + matePos);
            }
            if (dotSequence[i].equals("]")) {
                b.setPseudoKnot(true);
                b2 = tmpPseudoBases.pop();
                matePos = b2.getPosition();
                b.setMate(matePos);
                bases.add(b);
                //sets other end for mate base
                bases.get(matePos - 1).setMate(pos);
                knots.push(new Knot(matePos, pos, true));
                //System.out.println("Pseudoknot found between: " + b.getPosition() + " " + matePos);
            }
            if (dotSequence[i].equals(":")) {
                bases.add(b);
            }
        }

        if (debug) {
            for (Knot k : knots) {
                System.out.println(k.toString());
            }
        }
        structure.setKnots(knots);
        structure.setBases(bases);

        if (debug) {
            System.out.println("this is the bases array:");
            for (Base bas : bases) {
                System.out.println("basarr " + bas.getNucleotide() + " " + bas.getPosition() + " " + bas.getMate());
            }
        }
        return structure;
    }

    /**
     * addReactivity() takes the current structure containing a sequence of
     * nucleotides and adds reactivity values
     *
     * @param loadFile loadFile should contain new reactivity data
     */
    public void addReactivity(LoadFile loadFile) {
        if (structure != null) {
            try {
                structure = loadFile.getReactivityStructure(structure);
            } catch (IOException ex) {
                System.err.println("baseSequence and shapeReactivity size mismatch");
                Logger.getLogger(Model.class.getName()).log(Level.SEVERE, null, ex);
            }
            createStructure(structure);
            modelCircularFeynmanUnitCircle(structure);
        }
    }

    /**
     * modelCircularFeynmanUnitCircle plots coordinates for knots and bases into
     * a unit circle containing x and y coordinates ranging from -1 to 1 on both axes
     *
     * @param structure
     * @return
     */
    public Structure modelCircularFeynmanUnitCircle(Structure structure) {
        double unitcircleX, unitcircleY, theta;
        if (debug) {
            System.out.println("Starting modelCircularFeynman...");
        }

        bases = structure.getBases();
        LinkedList<Knot> knots = structure.getKnots();
        int nBases = bases.size();

        //loop to plot x and y coords for the unitcircle in bases array
        for (Base b : bases) {
            /**
             * 2 * Math.PI - circumference of the unit circle is 2*Math.PI
             * (b.getPosition() + 2) - leaves a space in the beginning of the
             * sequence (nBases + 5) - 5 leaves a room in the end of the
             * sequence "+(Math.PI / 2)" sets starting coordinates to the bottom
             * of the circle instead of to the right side
             */
            //theta = (((2 * Math.PI * (b.getPosition())) / (nBases)) + (Math.PI / 2));
            theta = (((2 * Math.PI * (b.getPosition() + 1)) / (nBases + 3)) + (Math.PI / 2));
            unitcircleX = Math.cos(theta);
            unitcircleY = Math.sin(theta);
            b.setUnitCircleX(unitcircleX);
            b.setUnitCircleY(unitcircleY);

            if (debug) {
                System.out.println("Unit circle coords: (" + unitcircleX + "," + unitcircleY + ")");
            }
        }

        //loop to plot knots
        for (Knot k : knots) {
            Base b1 = bases.get(k.getPos1() - 1);
            Base b2 = bases.get(k.getPos2() - 1);
            k.setUCArc(calcUnitCircleArc(b1, b2));
        }
        return structure;
    }

    public UnitCircleArc calcUnitCircleArc(Base ba1, Base ba2) {
        Base base1 = ba1;
        Base base2 = ba2;
        if (base1.getPosition() > base2.getPosition()) {
            Base tmpBase = base1;
            base1 = base2;
            base2 = tmpBase;
        }
        if ((base2.getPosition() - base1.getPosition()) > ((base1.getnBases()) / 2)) {
            Base tmpBase = base1;
            base1 = base2;
            base2 = tmpBase;
        }
        double b1x = base1.getUnitCircleX();
        double b1y = base1.getUnitCircleY();
        double b2x = base2.getUnitCircleX();
        double b2y = base2.getUnitCircleY();
        //cx & cy coords of the original circle
        double cx = 0.0;
        double cy = 0.0;
        //ccx & ccy coords of the complementary circle
        double ccx, ccy;
        //ccxstart & ccystart 0 degrees point for the complementary circle
        double ccxStart, ccyStart;
        // radius is always 1 in a unit circle
        double radius = getDistance(b1x, b1y, cy, cy);
        double m1, m2, b1, b2, sideB, sweep, oppositeStartAngle, arcRadius, startAngle;

        //Slope form:  m = (y2-y1)/(x2-x1)
        m1 = (b1y - cy) / (b1x - cx);
        m2 = (b2y - cy) / (b2x - cx);

        // Slope of tangent form: mt = -1 / m
        // if slope = 0 (vertical line) choose a high number (almost vertical)
        // to avoid NaN error
        if (m1 == 0) {
            m1 = 9999;
        } else {
            m1 = -1 / m1;
        }
        if (m2 == 0) {
            m2 = 9999;
        } else {
            m2 = -1 / m2;
        }
        /*
         * to isolate y-intercept (b) of the line: Slope intercept form: y = m *
         * x + b == b = y - (m*x)
         */

        b1 = b1y - (m1 * b1x);
        b2 = b2y - (m2 * b2x);

        /*
         * The X coord of the complementary circle (px) is found when setting
         * the y-coordinate of tangent 1 to the y-coordinate of tangent 2:
         * tangent1 = tangent2 == m1*x1+b1 = m2 * x2 * b2 == px = (b2 - b1) /
         * (m1 - m2)
         *
         * and then inserting x into the intercept-slope form to find y: py = (m
         * * x) +b
         */
        ccx = (b2 - b1) / (m1 - m2);
        ccy = (m1 * ccx) + b1;

        // Pythagoras to find radius of the complementary circle
        arcRadius = getDistance(b1x, b1y, ccx, ccy);
        if (debug) {
            System.out.println("arcRadius: " + arcRadius + " (" + b1x + "," + b1y + ")(" + ccx + "," + ccy + ")");
        }


        sideB = getDistance(cx, cy, ccx, ccy);

        //======== Cosinus relation. Her brugt til at beregne vinklen A //sweep vinklen
        // A = cos^-1((b*b+c*c-a*a)/)2*b*c))

        //radius = getDistance(b1x, b1y, cx, cy);
        sweep = ((sideB * sideB) + (arcRadius * arcRadius)
                - (radius * radius))
                / (2 * sideB * arcRadius);

        sweep = Math.toDegrees(Math.acos(sweep)) * 2;
        if (debug) {
            System.out.println("arcrad " + arcRadius + "SideB " + sideB + "sweepDegrees " + sweep);
        }
        // Calculate starting angle  =========================================================
        //Starting point for the complementary circle
        ccxStart = ccx + arcRadius;
        ccyStart = ccy;

        oppositeStartAngle = getDistance(b1x, b1y, ccxStart, ccyStart);

        //======== Cosinus relation. Her brugt til at beregne vinklen A 
        // A = cos^-1((b*b+c*c-a*a)/)2*b*c))

        startAngle = ((arcRadius * arcRadius) + (arcRadius * arcRadius)
                - (oppositeStartAngle * oppositeStartAngle))
                / (2 * arcRadius * arcRadius);
        startAngle = Math.acos(startAngle);
        startAngle = (Math.toDegrees(startAngle));
        if (b1x < cx) {
            startAngle = 360 - (startAngle);
        }
        UnitCircleArc ucarc = new UnitCircleArc(ccx, ccy, arcRadius * radius, startAngle, sweep, Arc2D.OPEN);
        return ucarc;
    }

    public double getDistance(double x1, double y1, double x2, double y2) {
        /**
         * |AB| = square root((x2-x1)^2 + (y2-y1)^2)
         */
        double distance = Math.sqrt(((x2 - x1) * (x2 - x1)) + ((y2 - y1) * (y2 - y1)));
        return distance;
    }

    /**
     * @param structure the structure to set
     */
    public void setStructure(Structure structure) {
        this.structure = structure;
    }

    /**
     * @param debug the debug to set
     */
    public void setDebug(boolean debug) {
        this.debug = debug;
    }

    /**
     * @return the structure
     */
    public Structure getStructure() {
        return structure;
    }

    /**
     * @return the gui
     */
    public GUI getGui() {
        return gui;
    }

    /**
     * @param gui the gui to set
     */
    public void setGui(GUI gui) {
        this.gui = gui;
    }

    /**
     * @return the controller
     */
    public Controller getController() {
        return controller;
    }

    /**
     * @param controller the controller to set
     */
    public void setController(Controller controller) {
        this.controller = controller;
    }

    boolean checkToDrawPosition(Base b) {
        int i = b.getPosition();
        int nBases = b.getnBases();
        return (i == 1 || i == nBases || i > 0 && (i % 10) == 0);
    }
}
